package com.zh.web.utils;

import cn.hutool.core.util.StrUtil;
import com.zh.common.dto.IpDto;
import com.zh.common.dto.UserAuthDto;
import com.zh.common.exception.AuthException;
import com.zh.common.exception.BusinessException;
import com.zh.common.utils.RedisUtil;

import com.zh.common.properties.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtUtil {

    private final JwtProperties jwtProperties;
    private final RedisUtil redisUtil;
    /**
     * redisKey的规则 jwtProperties.getRedisKeyPrefix()+jwt
     * 签发token就存入redis 刷新token删除旧的存储新的
     * Authentication的规则 tokenPrefix +jwt
     */
    private final String tokenPrefix = "Bearer ";

    /**
     * 认证用户签发jwt令牌
     *
     * @return jwt令牌
     */
    public String signToken(UserAuthDto userAuthDto) {
        try {
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", userAuthDto.getId());
            claims.put("rememberMe", userAuthDto.getRememberMe());
            claims.put("ipInfo", userAuthDto.getIpDto());
            long ttl = userAuthDto.getRememberMe() ? jwtProperties.getMaxTtl() : jwtProperties.getMinTtl();
            String jwt = this.createJWT(jwtProperties.getSecretKey(), ttl, claims);
            String authToken = tokenPrefix + jwt;
            String Key = jwtProperties.getRedisKeyPrefix() + jwt;
            this.invalidateOldToken(userAuthDto.getId());
            redisUtil.set(Key, claims, ttl, TimeUnit.DAYS);
            return authToken;
        } catch (Exception e) {
            log.error("签发token失败:{}", e.getMessage());
            throw new AuthException("签发token失败");
        }
    }

    /**
     * 解析token 获取用户信息
     *
     * @param authToken token 信息
     * @return UserAuthDto 用户认证信息
     */
    public UserAuthDto parseToken(String authToken) {
        try {
            String jwt = getJwt(authToken);
            Claims claims = parseJWT(jwtProperties.getSecretKey(), jwt);
            UserAuthDto userAuthDto = new UserAuthDto();
            userAuthDto.setId(Long.parseLong(claims.get("id").toString()));
            userAuthDto.setRememberMe((Boolean) claims.get("rememberMe"));
            Map<String, String> map = (Map<String, String>) claims.get("ipInfo");
            IpDto ipDto = new IpDto();
            ipDto.setIp(map.get("ip"));
            ipDto.setRegion(map.get("region"));
            ipDto.setCityInfo(map.get("cityInfo"));
            userAuthDto.setIpDto(ipDto);
            userAuthDto.setExpirationTime(claims.getExpiration().getTime());
            return userAuthDto;
        } catch (Exception e) {
            log.error("token无效:{}", e.getMessage(), e);
            throw new AuthException("token无效");
        }
    }

    /**
     * 刷新token
     *
     * @param authToken token
     * @return 刷新后的tokens
     */
    public String refreshToken(String authToken) {
        try {
            UserAuthDto userAuthDto = parseToken(authToken);
            invalidateToken(authToken);
            return signToken(userAuthDto);
        } catch (Exception e) {
            log.error("刷新token失败:{}", e.getMessage(), e);
            throw new BusinessException("刷新token失败");
        }
    }

    /**
     * 检测Token是否存在来判断是否已登录
     *
     * @param authToken
     * @return
     */
    public Boolean chickToken(String authToken) {
        try {
            String jwt = getJwt(authToken);
            String key = jwtProperties.getRedisKeyPrefix() + jwt;
            return redisUtil.exists(key);
        } catch (Exception e) {
            log.error("检查token失败:{}", e.getMessage(), e);
            throw new AuthException("token无效");
        }
    }

    /**
     * 使原有的 token 失效
     *
     * @param authToken 当前的 token
     */
    public void invalidateToken(String authToken) {
        try {
            String jwt = getJwt(authToken);
            String key = jwtProperties.getRedisKeyPrefix() + jwt;
            redisUtil.delete(key);
            log.info("Token {} 已失效", authToken);
        } catch (Exception e) {
            log.error("使token失效 失败: {}", e.getMessage(), e);
        }
    }

    /**
     * 签发token时删除旧的token
     *
     * @param id 用户id
     */
    private void invalidateOldToken(Long id) {
        //删除旧的token
        List<String> keys = redisUtil.getKeysByPrefix(jwtProperties.getRedisKeyPrefix(), true);
        List<String> keysToDelete = new ArrayList<>();
        keys.stream().forEach(key -> {
            Map<String, Object> map = redisUtil.get(key);
            if (map.get("id").equals(id)) {
                keysToDelete.add(key);
            }
        });
        redisUtil.deleteKeys(keysToDelete);
    }

    /**
     * 获取真正的jwt令牌
     *
     * @param authToken
     * @return
     */
    public String getJwt(String authToken) {
        if (StrUtil.isEmpty(authToken)) {
            return null;
        }
        String jwt = authToken.substring(tokenPrefix.length());
        return jwt;
    }

    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return jwt
     */
    private String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法，也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis * 1000;
        Date exp = new Date(expMillis);
        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明，一定要先设置这个自己创建的私有的声明，这个是给builder的claim赋值，一旦写在标准的声明赋值之后，就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);
        return builder.compact();
    }

    /**
     * Token解码
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return 解密后的数据
     */
    private Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        return Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
    }
}